package bgpemulation;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.TreeSet;

import network.API;
import network.ErrorCause;
import network.Message;

import org.w3c.dom.Document;

import eventengine.Event;

public class AS extends API{
	
	private static final int MIN_PORT = 9000;
	
	public static final String PEER = "PEER";
	public static final String PROTOCOL = "PROTOCOL";
	public static final String BOTH = "BOTH";
	public static final int MAX_START_DELAY = 5000;
	public static final int MIN_ORIGIN_FREQ = 1000;
	public static final int MAX_ORIGIN_FREQ = 10000;
	public static final int MAX_WITHDRAW_DELAY = 10000;
	public static final int MIN_WITHDRAW_FREQ = 5000;
	public static final int MAX_WITHDRAW_FREQ = 20000;
	public static final int MAX_PREFIXES = 5;
	
	public static final int UPDATE = 2;
	public static final int CONNECTION = 1;		// hello message to establish peering
	public static final int PROTOCOL_MSG = 0;		// 
	public static final int BGP_PEER = 0;
	public static final int PROTO_PEER = 1;
	
	private Map<Integer, String> bgpNeighbors = new HashMap<Integer, String>();
	protected TreeSet<String> protocolNeighbors = new TreeSet<String>();
	
	// When a prefix is first originated, keep track of it so we can later withdraw it.
	protected List<CIDRPrefix> originatedPrefixes = new LinkedList<CIDRPrefix>();
	private int origin;
	private BGPMessage BGPMessageHandler = new BGPMessage();
	private Random rng;
	private int asNumber;
	
	protected BGPRoutingTable bgpRoutingTable = new BGPRoutingTable();
	private IPRoutingTable ipRoutingTable = new IPRoutingTable();
	
	public AS(int tcpServerPort, int udpChannelPort, String ip,
			Document config, String networkLogFile, String applicationLogFile) {
		super(udpChannelPort, udpChannelPort, ip, config, networkLogFile, applicationLogFile);
		
		origin = Integer.parseInt(networkLogFile.substring(4, 5));
		rng = new Random(origin * 10000000);
		this.asNumber = tcpServerPort;
	}

	@Override
	public void handleError(String ip, ErrorCause cause) {
		System.err.println("ERROR: " + ip + " " + cause);
	}

	@Override
	public void handleError(Message message, ErrorCause cause) {
		System.err.println("ERROR: " + message.getDest() + " " + message.getSource() + " " + cause);
	}

	@Override
	public void acceptedTCPConnection(String address) {
		System.out.println("Accepted TCP connection from: " + address);
	}

	@Override
	public void receiveMessage(Message message) {
		
		System.out.println("Receiving message at time: " + adjustTime(System.currentTimeMillis()));
		System.out.println("\tReceived message from: " + message.getSource());
		byte[] payload = message.getPayload();
		ByteBuffer bb = ByteBuffer.wrap(payload);

		int messageType = (int)bb.get();
		System.out.println("\tmessage type: " + messageType);		
		
		switch(messageType)
		{
		case UPDATE:
			// update function
			System.out.println("RECEIVING UPDATE");
			receiveBGPUpdateMessage(bb);
			break;
		case CONNECTION:
			System.out.println("RECEIVING CONNECTION MESSAGE");
			parseHello(bb.get(), message);
			break;
		case PROTOCOL_MSG:
			System.out.println("RECEIVING PROTOCOL MESSAGE");
			protocolMessage(message);
			break;
		}
	}

	public void protocolMessage(Message message) {
		System.out.println("WTF IS THIS?");
	}

	private void receiveBGPUpdateMessage(ByteBuffer bb) {
		System.out.println("\treceiveBGPUpdateMessage has been called! Doing stuff...");
		
		List<CIDRPrefix> withdrawnIPs = BGPMessageHandler.getWithdrawnIPs(bb);
		List<CIDRPrefix> reachableIPs = BGPMessageHandler.getReachableIPs(bb);
		
		System.out.println("withdrawn IPs: \t" + withdrawnIPs);
		System.out.println("reachable IPs: \t" + reachableIPs);
		
		List<Byte> asPath = BGPMessageHandler.getASPath(bb, withdrawnIPs, reachableIPs);
		if(asPath.contains(asNumber())){
			return;
		}
		System.out.println("AS Path: \t\t" + toString(asPath));
		for(CIDRPrefix prefix : withdrawnIPs){
			if(removeASPath(prefix, asPath)){
				newBestPath(prefix);
			}
		}
		for(CIDRPrefix prefix : reachableIPs){
			if(addASPath(prefix, asPath)){
				newBestPath(prefix);
			}
		}
		if(shouldForward(asPath) && !withdrawnIPs.isEmpty()){
			forward(asPath, withdrawnIPs, reachableIPs);
		}
	}
	
	private void newBestPath(CIDRPrefix prefix) {
		PriorityQueue<ASPath> paths = bgpRoutingTable.get(prefix);
		if(paths==null){
			return;
		}
		ASPath ap = paths.peek();
		List<CIDRPrefix> reachable = new LinkedList<CIDRPrefix>();
		reachable.add(prefix);
		if(ap!=null){
			forward(toBytes(ap), new LinkedList<CIDRPrefix>(), reachable);
		}
	}

	private List<Byte> toBytes(ASPath ap) {
		int[] arr = ap.getArray();
		LinkedList<Byte> bytes = new LinkedList<Byte>();
		for(int as : arr){
			bytes.add((byte)as);
		}
		return bytes;
	}

	private boolean shouldForward(List<Byte> asPath) {
		if(asPath.contains(asNumber())){
			return false;
		}
		return true;
	}

	private void forward(List<Byte> asPath, List<CIDRPrefix> withdrawn,	List<CIDRPrefix> reachable) {
		asPath.add(0, asNumber());
		byte[] messagePayload = createBGPMessagePayload(UPDATE, withdrawn, reachable, asPath);
		Collection<String> neighbours = bgpNeighbors.values();
		broadcast(neighbours, messagePayload, API.TCP, false);
	}

	private boolean addASPath(CIDRPrefix prefix, List<Byte> asPath) {
		if(bgpRoutingTable.put(prefix, toString(asPath))){
			ipRoutingTable.put(prefix, nextHop(asPath));
			return true;
		}
		return false;
	}

	private String nextHop(List<Byte> asPath) {
		return "<IP OF AS#" + asPath.get(0) + ">";
	}

	private boolean removeASPath(CIDRPrefix prefix, List<Byte> asPath) {
		if(bgpRoutingTable.remove(prefix, toString(asPath))){
			ipRoutingTable.remove(prefix, nextHop(asPath));
			return true;
		}
		return false;
	}

	private String toString(List<Byte> asPath) {
		StringBuilder sb = new StringBuilder();
		for(Byte b : asPath){
			sb.append(b);
			sb.append(ASPath.ASN_SEP);
		}
		return sb.toString().replaceAll(ASPath.ASN_SEP + "$", "");
	}

	private void parseHello(int type, Message msg) {
		System.out.print("Received hello message from " + msg.getSource());
		if(type==BGP_PEER){
			System.out.println(" (BGP)");
			bgpNeighbors.put(getPort(msg.getSource()), msg.getSource());
		} else if(type==PROTO_PEER){
			System.out.println(" (TP)");
			protocolNeighbors.add(msg.getSource());
		}
	}

	private Integer getPort(String source) {
		return Integer.parseInt(source.split(":")[1]);
	}

	@Override
	public void connectionCompleted(String remoteAddress) {
		System.out.println("Completed TCP connection to: " + remoteAddress);
	}
	
	public byte[] createBGPMessagePayload(int msgType, List<CIDRPrefix> withdrawnPrefixes, List<CIDRPrefix> newPrefixes, List<Byte> asPath)
	{	
		System.out.println("Creating new BGP message");
		
		return BGPMessageHandler.createBGPMessage(msgType, withdrawnPrefixes, newPrefixes, asPath);
		
	}

	@Override
	public void updateTopology(String remoteAddress, String protocol,
			String direction) {
		if(direction.matches(PEER)){
			bgpNeighbors.put(getPort(remoteAddress), remoteAddress);
		}else if(direction.matches(PROTOCOL)){
			protocolNeighbors.add(remoteAddress);
		} else if(direction.matches(BOTH)){
			bgpNeighbors.put(getPort(remoteAddress), remoteAddress);
			protocolNeighbors.add(remoteAddress);
		}
	}

	@Override
	public void start() {
		for(Integer asn : bgpNeighbors.keySet()){
			bgpHello(bgpNeighbors.get(asn));
		}
		for(String peer : protocolNeighbors){
			protocolHello(peer);
		}
		startOriginating();
		startWithdrawing();
	}

	private void protocolHello(String peer) {
		hello(PROTO_PEER, peer);
	}

	private void bgpHello(String peer) {
		hello(BGP_PEER, peer);
	}

	private void hello(int type, String peer) {
		ByteBuffer bb = ByteBuffer.wrap(new byte[2]);
		bb.put((byte)CONNECTION);
		bb.put((byte)type);
		Message m = new Message(peer, bb.array());
		sendTCP(m, false);
	}

	private void startWithdrawing() {
		long start = rng.nextInt(MAX_WITHDRAW_DELAY);
		long freq = randomInt(MIN_WITHDRAW_FREQ, MAX_WITHDRAW_FREQ);
		Event e = new Event(start, freq, null){
			@Override
			public void execute(){
				withdraw();
			}
		};
		schedule(e);
	}

	private void withdraw() {
		List<CIDRPrefix> withdrawnPrefixes = new LinkedList<CIDRPrefix>();
		int numPrefix = randomInt(1, MAX_PREFIXES);
		for(int i = 0; i<numPrefix; i++){
			if(originatedPrefixes.isEmpty()){
				break;
			}
			int p = rng.nextInt(originatedPrefixes.size());
			CIDRPrefix prefix = originatedPrefixes.remove(p);
			withdrawnPrefixes.add(prefix);
		}
		withdraw(withdrawnPrefixes);
	}

	private void withdraw(List<CIDRPrefix> withdrawnPrefixes) {
		if(withdrawnPrefixes.isEmpty()){
			return;
		}
		System.out.println();
		System.out.println("Withdrawing:");
		printList(withdrawnPrefixes);
		forward(new LinkedList<Byte>(), withdrawnPrefixes, new LinkedList<CIDRPrefix>());
	}

	private void startOriginating() {
		long start = rng.nextInt(MAX_START_DELAY);
		long freq = randomInt(MIN_ORIGIN_FREQ, MAX_ORIGIN_FREQ);
		Event e = new Event(start, freq, null){
			@Override
			public void execute(){
				originate();
			}
		};
		schedule(e);
	}

	private void originate() {
		List<CIDRPrefix> newPrefixes = new LinkedList<CIDRPrefix>();
		int numPrefix = randomInt(1, MAX_PREFIXES);
		for(int i = 0; i<numPrefix; i++){
			CIDRPrefix prefix = CIDRPrefix.randomPrefix(rng.nextLong());
			newPrefixes.add(prefix);
			originatedPrefixes.add(prefix);
		}
		originate(newPrefixes);
	}

	private void originate(List<CIDRPrefix> newPrefixes) {
		System.out.println();
		System.out.println("Originated:");
		printList(newPrefixes);
		forward(new LinkedList<Byte>(), new LinkedList<CIDRPrefix>(), newPrefixes);
	}
	
	protected Byte asNumber() {
		return (byte) (asNumber - MIN_PORT);
	}

	private void printList(Iterable<?> objects) {
		for(Object o : objects){
			System.out.println(o.toString());
		}
	}

	public int randomInt(int min, int max) {
		return rng.nextInt(max-min) + min;
	}

	@Override
	public void stop() {
		try{
			System.out.println("Node stopped.");
			System.out.println("BGP NEIGHBORS:");
			printList(bgpNeighbors.values());
			System.out.println("PROTOCOL NEIGHBORS:");
			printList(protocolNeighbors);
			printRoutingTables();
			makeRoutingTableGraph();
			System.exit(0);
		} catch(Exception e){
			e.printStackTrace();
			System.exit(0);
		}
	}

	private void makeRoutingTableGraph() throws IOException {
		BGPGraph bgpGraph = new BGPGraph(asNumber(), bgpRoutingTable, originatedPrefixes);
		BufferedWriter bw = new BufferedWriter(new FileWriter("gvNode" + asNumber() + ".txt"));
		bgpGraph.printGraphViz(bw);
		bw.flush();
		bw.close();
	}

	private void printRoutingTables() {
		System.out.println("BGP ROUTING TABLE:");
		bgpRoutingTable.print();
		System.out.println("IP ROUTING TABLE:");
		ipRoutingTable.print();
	}

	@Override
	public void sendComplete(Message message) {
		System.out.println("This should probably not print.");
	}	
}
